﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using Gumini.JulaDB.DataTypes;
using Gumini.JulaDB.Expressions;
using Gumini.JulaDB.Interfaces;
using Gumini.JulaDB.Provider;
using Gumini.JulaDB.Enums;

namespace Gumini.JulaDB.Data
{
    /// <summary>
    /// An ordered collection of column definitions.
    /// Immutable.
    /// </summary>
    [Serializable]
    public class TableHeading<C> : IHeading<C>, ISerializable
        where C : class, IColumn
    {
        private readonly List<C> _columnList = new List<C>();
        private readonly Context<C> _columnContext = new Context<C>();

        public TableHeading(IEnumerable<C> columnDefinitions)
        {
            _columnList.AddRange(columnDefinitions);
            InitializeContext();
        }

        private void InitializeContext()
        {
            foreach (var column in _columnList)
                foreach (var columnName in column.AllNames)
                    _columnContext.Add(key: columnName, value: column);
        }

        /// <summary>
        /// Check if the column names are unique. If not, throws an exception.
        /// </summary>
        public void CheckNamesUnique()
        {
            if (_columnList.Select(c => c.ColumnName).Distinct().Count() < _columnList.Count)
                throw new JulaDbException(ErrorCode.DuplicateColumnName, "All column names within a table must be unique.");
        }

        /// <summary>
        /// Returns a zero-based ordinal for the column of the given name.
        /// </summary>
        public int GetColumnOrdinal(string columnName)
        {
            var column = _columnContext[columnName];
            int index = _columnList.FindIndex(d => d.Equals(column));
            return index;
        }

        /// <summary>
        /// Returns true if the column with the given name exists.
        /// </summary>
        public bool ContainsColumn(string columnName)
        {
            return _columnContext.ContainsKey(columnName);
        }

        public int NumberOfColumns
        {
            get { return _columnList.Count; }
        }

        public C this[int i]
        {
            get
            {
                if (i < 0 || i >= _columnList.Count)
                    throw new ArgumentOutOfRangeException(String.Format("Index must be in range 0-{0}", _columnList.Count - 1));
                return _columnList[i];
            }
        }

        public C this[string name]
        {
            get { return _columnContext[name]; }
        }

        public Context<DataType> GetTypeContext()
        {
            // creating the type context for deriving expression types
            var context = new Context<DataType>();
            for (int i = 0; i < NumberOfColumns; i++)
            {
                var definition = this[i];
                foreach(var name in definition.AllNames)
                    context.Add(name, definition.Type);
            }
            return context;
        }

        public override string ToString()
        {
            return string.Format("({0})", string.Join(", ", _columnList.Select(c => c.ColumnName).ToArray()));
        }

        public IEnumerator<C> GetEnumerator()
        {
            return _columnList.GetEnumerator();
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return _columnList.GetEnumerator();
        }

        #region Serialization

        private const string ColumnListName = "ColumnList";

        public TableHeading(SerializationInfo info, StreamingContext context)
        {
            _columnList = (List<C>)info.GetValue(ColumnListName, typeof(List<C>));
        }

        [OnDeserialized]
        private void SetValuesOnDeserialized(StreamingContext context)
        {
            InitializeContext();
        }

        public void GetObjectData(SerializationInfo info, StreamingContext context)
        {
            info.AddValue(ColumnListName, _columnList, typeof(List<C>));
        }

        #endregion
    }
}
